/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.ext.xerces.impl.dv.xs;

import java.math.BigDecimal;

import javax.xml.datatype.DatatypeFactory;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;

import org.apache.jena.ext.xerces.DatatypeFactoryInst;
import org.apache.jena.ext.xerces.impl.Constants;
import org.apache.jena.ext.xerces.xs.datatypes.XSDateTime;

/**
 * This is the base class of all date/time datatype validators.
 * It implements common code for parsing, validating and comparing datatypes.
 * Classes that extend this class, must implement parse() method.
 *
 * REVISIT: There are many instance variables, which would cause problems
 *          when we support grammar caching. A grammar is possibly used by
 *          two parser instances at the same time, then the same simple type
 *          decl object can be used to validate two strings at the same time.
 *          -SG
 *          
 * {@literal @xerces.internal} 
 *
 * @author Elena Litani
 * @author Len Berman
 * @author Gopal Sharma, SUN Microsystems Inc.
 *
 * @version $Id: AbstractDateTimeDV.java 965250 2010-07-18 16:04:58Z mrglavas $
 */
@SuppressWarnings("all")
public abstract class AbstractDateTimeDV extends TypeValidator {
	
	//debugging
	private static final boolean DEBUG=false;
	
	//define shared variables for date/time
	
	
	//define constants to be used in assigning default values for
	//all date/time excluding duration
	protected final static int YEAR=2000;
	protected final static int MONTH=01;
	protected final static int DAY = 01;
    
    protected static final DatatypeFactory datatypeFactory = DatatypeFactoryInst.newDatatypeFactory();
	
    public short getAllowedFacets(){
		return ( XSSimpleTypeDecl.FACET_PATTERN | XSSimpleTypeDecl.FACET_WHITESPACE | XSSimpleTypeDecl.FACET_ENUMERATION |XSSimpleTypeDecl.FACET_MAXINCLUSIVE |XSSimpleTypeDecl.FACET_MININCLUSIVE | XSSimpleTypeDecl.FACET_MAXEXCLUSIVE  | XSSimpleTypeDecl.FACET_MINEXCLUSIVE  );
	}//getAllowedFacets()
	
	
	// distinguishes between identity and equality for date/time values
	// ie: two values representing the same "moment in time" but with different 
	// remembered timezones are now equal but not identical.
    public boolean isIdentical (Object value1, Object value2) {
		if (!(value1 instanceof DateTimeData) || !(value2 instanceof DateTimeData)) {
			return false;
		}
		
		DateTimeData v1 = (DateTimeData)value1;
		DateTimeData v2 = (DateTimeData)value2;
		
		// original timezones must be the same in addition to date/time values
		// being 'equal'
		if ((v1.timezoneHr == v2.timezoneHr) && (v1.timezoneMin == v2.timezoneMin)) {
			return v1.equals(v2);
		}
		
		return false;
	}//isIdentical()
	
	// the parameters are in compiled form (from getActualValue)
    public int compare (Object value1, Object value2) {
		return compareDates(((DateTimeData)value1),
				((DateTimeData)value2), true);
	}//compare()
	
	/**
	 * Compare algorithm described in dateDime (3.2.7).
	 * Duration datatype overwrites this method
	 *
	 * @param date1  normalized date representation of the first value
	 * @param date2  normalized date representation of the second value
	 * @param strict
	 * @return less, greater, less_equal, greater_equal, equal
	 */
	protected short compareDates(DateTimeData date1, DateTimeData date2, boolean strict) {
		if (date1.utc == date2.utc) {
			return compareOrder(date1, date2);
		}
		short c1, c2;
		
		DateTimeData tempDate = new DateTimeData(null, this);
		
		if ( date1.utc=='Z' ) {
			
			//compare date1<=date1<=(date2 with time zone -14)
			//
			cloneDate(date2, tempDate); //clones date1 value to global temporary storage: fTempDate
			tempDate.timezoneHr=14;
			tempDate.timezoneMin = 0;
			tempDate.utc='+';
			normalize(tempDate);
			c1 = compareOrder(date1, tempDate);
			if (c1 == LESS_THAN)
				return c1;
			
			//compare date1>=(date2 with time zone +14)
			//
			cloneDate(date2, tempDate); //clones date1 value to global temporary storage: tempDate
			tempDate.timezoneHr = -14;
			tempDate.timezoneMin = 0;
			tempDate.utc='-';
			normalize(tempDate);
			c2 = compareOrder(date1, tempDate);
			if (c2 == GREATER_THAN)
				return c2;
			
			return INDETERMINATE;
		}
		else if ( date2.utc=='Z' ) {
			
			//compare (date1 with time zone -14)<=date2
			//
			cloneDate(date1, tempDate); //clones date1 value to global temporary storage: tempDate
			tempDate.timezoneHr = -14;
			tempDate.timezoneMin = 0;
			tempDate.utc='-';
			if (DEBUG) {
				System.out.println("tempDate=" + dateToString(tempDate));
			}
			normalize(tempDate);
			c1 = compareOrder(tempDate, date2);
			if (DEBUG) {
				System.out.println("date=" + dateToString(date2));
				System.out.println("tempDate=" + dateToString(tempDate));
			}
			if (c1 == LESS_THAN)
				return c1;
			
			//compare (date1 with time zone +14)<=date2
			//
			cloneDate(date1, tempDate); //clones date1 value to global temporary storage: tempDate
			tempDate.timezoneHr = 14;
			tempDate.timezoneMin = 0;
			tempDate.utc='+';
			normalize(tempDate);
			c2 = compareOrder(tempDate, date2);
			if (DEBUG) {
				System.out.println("tempDate=" + dateToString(tempDate));
			}
			if (c2 == GREATER_THAN)
				return c2;
			
			return INDETERMINATE;
		}
		return INDETERMINATE;
		
	}
	
	/**
	 * Given normalized values, determines order-relation
	 * between give date/time objects.
	 *
	 * @param date1  date/time object
	 * @param date2  date/time object
	 * @return 0 if date1 and date2 are equal, a value less than 0 if date1 is less than date2, a value greater than 0 if date1 is greater than date2
	 */
	protected short compareOrder(DateTimeData date1, DateTimeData date2) {
		if(date1.position < 1) {
			if (date1.year < date2.year)
				return -1;
			if (date1.year > date2.year)
				return 1;
		}
		if(date1.position < 2) {
			if (date1.month < date2.month)
				return -1;
			if (date1.month > date2.month)
				return 1;
		}
		if (date1.day < date2.day)
			return -1;
		if (date1.day > date2.day)
			return 1;
		if (date1.hour < date2.hour)
			return -1;
		if (date1.hour > date2.hour)
			return 1;
		if (date1.minute < date2.minute)
			return -1;
		if (date1.minute > date2.minute)
			return 1;
		if (date1.second < date2.second)
			return -1;
		if (date1.second > date2.second)
			return 1;
		if (date1.utc < date2.utc)
			return -1;
		if (date1.utc > date2.utc)
			return 1;
		return 0;
	}
	
	/**
	 * Parses time hh:mm:ss.sss and time zone if any
	 *
	 * @param start
	 * @param end
	 * @param data
	 * @exception RuntimeException
	 */
	protected  void getTime (String buffer, int start, int end, DateTimeData data) throws RuntimeException{
		
		int stop = start+2;
		
		//get hours (hh)
		data.hour=parseInt(buffer, start,stop);
		
		//get minutes (mm)
		
		if (buffer.charAt(stop++)!=':') {
			throw new RuntimeException("Error in parsing time zone" );
		}
		start = stop;
		stop = stop+2;
		data.minute=parseInt(buffer, start,stop);
		
		//get seconds (ss)
		if (buffer.charAt(stop++)!=':') {
			throw new RuntimeException("Error in parsing time zone" );
		}
		
		//find UTC sign if any
		int sign = findUTCSign(buffer, start, end);
		
		//get seconds (ms)
		start = stop;
		stop = sign < 0 ? end : sign;
		data.second = parseSecond(buffer, start, stop);
		
		//parse UTC time zone (hh:mm)
		if (sign > 0) {
			getTimeZone(buffer, data, sign, end);
		}
	}
	
	/**
	 * Parses date CCYY-MM-DD
	 *
	 * @param buffer
	 * @param start start position
	 * @param end end position
	 * @param date
	 * @exception RuntimeException
	 */
	protected int getDate (String buffer, int start, int end, DateTimeData date) throws RuntimeException{
		
		start = getYearMonth(buffer, start, end, date);
		
		if (buffer.charAt(start++) !='-') {
			throw new RuntimeException("CCYY-MM must be followed by '-' sign");
		}
		int stop = start + 2;
		date.day=parseInt(buffer, start, stop);
		return stop;
	}
	
	/**
	 * Parses date CCYY-MM
	 *
	 * @param buffer
	 * @param start start position
	 * @param end end position
	 * @param date
	 * @exception RuntimeException
	 */
	protected int getYearMonth (String buffer, int start, int end, DateTimeData date) throws RuntimeException{
		
		if ( buffer.charAt(0)=='-' ) {
			// REVISIT: date starts with preceding '-' sign
			//          do we have to do anything with it?
			//
			start++;
		}
		int i = indexOf(buffer, start, end, '-');
		if ( i==-1 ) throw new RuntimeException("Year separator is missing or misplaced");
		int length = i-start;
		if (length<4) {
			throw new RuntimeException("Year must have 'CCYY' format");
		}
		else if (length > 4 && buffer.charAt(start)=='0'){
			throw new RuntimeException("Leading zeros are required if the year value would otherwise have fewer than four digits; otherwise they are forbidden");
		}
		date.year= parseIntYear(buffer, i);
		if (buffer.charAt(i)!='-') {
			throw new RuntimeException("CCYY must be followed by '-' sign");
		}
		start = ++i;
		i = start +2;
		date.month=parseInt(buffer, start, i);
		return i; //fStart points right after the MONTH
	}
	
	/**
	 * Shared code from Date and YearMonth datatypes.
	 * Finds if time zone sign is present
	 *
	 * @param end
	 * @param date
	 * @exception RuntimeException
	 */
	protected void parseTimeZone (String buffer, int start, int end, DateTimeData date) throws RuntimeException{
		
		//fStart points right after the date
		
		if ( start < end ) {
			if (!isNextCharUTCSign(buffer, start, end)) {
				throw new RuntimeException ("Error in month parsing");
			}
			else {
				getTimeZone(buffer, date, start, end);
			}
		}
	}
	
	/**
	 * Parses time zone: 'Z' or {+,-} followed by  hh:mm
	 *
	 * @param data
	 * @param sign
	 * @exception RuntimeException
	 */
	protected void getTimeZone (String buffer, DateTimeData data, int sign, int end) throws RuntimeException{
		data.utc=buffer.charAt(sign);
		
		if ( buffer.charAt(sign) == 'Z' ) {
			if (end>(++sign)) {
				throw new RuntimeException("Error in parsing time zone");
			}
			return;
		}
		if ( sign<=(end-6) ) {
			
			int negate = buffer.charAt(sign) == '-'?-1:1;
			//parse hr
			int stop = ++sign+2;
			data.timezoneHr = negate*parseInt(buffer, sign, stop);
			if (buffer.charAt(stop++)!=':') {
				throw new RuntimeException("Error in parsing time zone" );
			}
			
			//parse min
			data.timezoneMin = negate*parseInt(buffer, stop, stop+2);
			
			if ( stop+2!=end ) {
				throw new RuntimeException("Error in parsing time zone");
			}
            if(data.timezoneHr != 0 || data.timezoneMin != 0)
                data.normalized = false;
		}
		else {
			throw new RuntimeException("Error in parsing time zone");
		}
		if ( DEBUG ) {
			System.out.println("time[hh]="+data.timezoneHr + " time[mm]=" +data.timezoneMin);
		}
	}
	
	/**
	 * Computes index of given char within StringBuffer
	 *
	 * @param start
	 * @param end
	 * @param ch     character to look for in StringBuffer
	 * @return index of ch within StringBuffer
	 */
	protected  int indexOf (String buffer, int start, int end, char ch) {
		for ( int i=start;i<end;i++ ) {
			if ( buffer.charAt(i) == ch ) {
				return i;
			}
		}
		return -1;
	}
	
	/**
	 * Validates given date/time object accoring to W3C PR Schema
	 * [D.1 ISO 8601 Conventions]
	 *
	 * @param data
	 */
	protected void validateDateTime (DateTimeData data) {
		
		//REVISIT: should we throw an exception for not valid dates
		//          or reporting an error message should be sufficient?
		
		/**
		 * XML Schema 1.1 - RQ-123: Allow year 0000 in date related types.
		 */
		if (!Constants.SCHEMA_1_1_SUPPORT && data.year==0 ) {
			throw new RuntimeException("The year \"0000\" is an illegal year value");
			
		}
		
		if ( data.month<1 || data.month>12 ) {
			throw new RuntimeException("The month must have values 1 to 12");
			
		}
		
		//validate days
		if ( data.day>maxDayInMonthFor(data.year, data.month) || data.day<1 ) {
			throw new RuntimeException("The day must have values 1 to 31");
		}
		
		//validate hours
		if ( data.hour>23 || data.hour<0 ) {
			if (data.hour == 24 && data.minute == 0 && data.second == 0) {
				data.hour = 0;
				if (++data.day > maxDayInMonthFor(data.year, data.month)) {
					data.day = 1;
					if (++data.month > 12) {
						data.month = 1;
						if (Constants.SCHEMA_1_1_SUPPORT) {
							++data.year;
						}
						else if (++data.year == 0) {
							data.year = 1;
						}
					}
				}
			}
			else {
				throw new RuntimeException("Hour must have values 0-23, unless 24:00:00");
			}
		}
		
		//validate
		if ( data.minute>59 || data.minute<0 ) {
			throw new RuntimeException("Minute must have values 0-59");
		}
		
		//validate
		if ( data.second>=60 || data.second<0 ) {
			throw new RuntimeException("Second must have values 0-59");
			
		}
		
		//validate
		if ( data.timezoneHr>14 || data.timezoneHr<-14 ) {
			throw new RuntimeException("Time zone should have range -14:00 to +14:00");
		}
		else {
			if((data.timezoneHr == 14 || data.timezoneHr == -14) && data.timezoneMin != 0)
				throw new RuntimeException("Time zone should have range -14:00 to +14:00");
			else if(data.timezoneMin > 59 || data.timezoneMin < -59)
				throw new RuntimeException("Minute must have values 0-59");
		}
		
	}
	
	/**
	 * Return index of UTC char: 'Z', '+', '-'
	 *
	 * @param start
	 * @param end
	 * @return index of the UTC character that was found
	 */
	protected int findUTCSign (String buffer, int start, int end) {
		int c;
		for ( int i=start;i<end;i++ ) {
			c=buffer.charAt(i);
			if ( c == 'Z' || c=='+' || c=='-' ) {
				return i;
			}
			
		}
		return -1;
	}
    
    /**
     * Returns <code>true</code> if the character at start is 'Z', '+' or '-'.
     */
    protected final boolean isNextCharUTCSign(String buffer, int start, int end) {
        if (start < end) {
            char c = buffer.charAt(start);
            return (c == 'Z' || c == '+' || c == '-');
        }
        return false;
    }
	
	/**
	 * Given start and end position, parses string value
	 *
	 * @param buffer string to parse
	 * @param start  start position
	 * @param end    end position
	 * @return  return integer representation of characters
	 */
	protected  int parseInt (String buffer, int start, int end)
	throws NumberFormatException{
		//REVISIT: more testing on this parsing needs to be done.
		int radix=10;
		int result = 0;
		int digit=0;
		int limit = -Integer.MAX_VALUE;
		int multmin = limit / radix;
		int i = start;
		do {
			digit = getDigit(buffer.charAt(i));
			if ( digit < 0 ) throw new NumberFormatException("'" + buffer + "' has wrong format");
			if ( result < multmin ) throw new NumberFormatException("'" + buffer + "' has wrong format");
			result *= radix;
			if ( result < limit + digit ) throw new NumberFormatException("'" + buffer + "' has wrong format");
			result -= digit;
			
		}while ( ++i < end );
		return -result;
	}
	
	// parse Year differently to support negative value.
	protected int parseIntYear (String buffer, int end){
		int radix=10;
		int result = 0;
		boolean negative = false;
		int i=0;
		int limit;
		int multmin;
		int digit=0;
		
		if (buffer.charAt(0) == '-'){
			negative = true;
			limit = Integer.MIN_VALUE;
			i++;
			
		}
		else{
			limit = -Integer.MAX_VALUE;
		}
		multmin = limit / radix;
		while (i < end)
		{
			digit = getDigit(buffer.charAt(i++));
			if (digit < 0) throw new NumberFormatException("'" + buffer + "' has wrong format");
			if (result < multmin) throw new NumberFormatException("'" + buffer + "' has wrong format");
			result *= radix;
			if (result < limit + digit) throw new NumberFormatException("'" + buffer + "' has wrong format");
			result -= digit;
		}
		
		if (negative)
		{
			if (i > 1) return result;
			else throw new NumberFormatException("'" + buffer + "' has wrong format");
		}
		return -result;
		
	}
	
	/**
	 * If timezone present - normalize dateTime  [E Adding durations to dateTimes]
	 *
	 * @param date   CCYY-MM-DDThh:mm:ss+03
	 */
	protected void normalize(DateTimeData date) {
		
		// REVISIT: we have common code in addDuration() for durations
		//          should consider reorganizing it.
		//
		
		//add minutes (from time zone)
		int negate = -1;
		
		if ( DEBUG ) {
			System.out.println("==>date.minute"+date.minute);
			System.out.println("==>date.timezoneMin" +date.timezoneMin);
		}
		int temp = date.minute + negate * date.timezoneMin;
		int carry = fQuotient (temp, 60);
		date.minute= mod(temp, 60, carry);
		
		if ( DEBUG ) {
			System.out.println("==>carry: " + carry);
		}
		//add hours
		temp = date.hour + negate * date.timezoneHr + carry;
		carry = fQuotient(temp, 24);
		date.hour=mod(temp, 24, carry);
		if ( DEBUG ) {
			System.out.println("==>date.hour"+date.hour);
			System.out.println("==>carry: " + carry);
		}
		
		date.day=date.day+carry;
		
		while ( true ) {
			temp=maxDayInMonthFor(date.year, date.month);
			if (date.day<1) {
				date.day = date.day + maxDayInMonthFor(date.year, date.month-1);
				carry=-1;
			}
			else if ( date.day>temp ) {
				date.day=date.day-temp;
				carry=1;
			}
			else {
				break;
			}
			temp=date.month+carry;
			date.month=modulo(temp, 1, 13);
			date.year=date.year+fQuotient(temp, 1, 13);
            if(date.year == 0 && !Constants.SCHEMA_1_1_SUPPORT) {
                date.year = (date.timezoneHr < 0 || date.timezoneMin < 0)?1:-1;
            }
		}
		date.utc='Z';
	}
	
	
	/**
     * @param date
     */
    protected void saveUnnormalized(DateTimeData date) {
        date.unNormYear = date.year;
        date.unNormMonth = date.month;
        date.unNormDay = date.day;
        date.unNormHour = date.hour;
        date.unNormMinute = date.minute;
        date.unNormSecond = date.second;
    }

    /**
	 * Resets object representation of date/time
	 *
	 * @param data   date/time object
	 */
	protected void resetDateObj(DateTimeData data) {
		data.year = 0;
		data.month = 0;
		data.day = 0;
		data.hour = 0;
		data.minute = 0;
		data.second = 0;
		data.utc = 0;
		data.timezoneHr = 0;
		data.timezoneMin = 0;
	}
	
	/**
	 * Given {year,month} computes maximum
	 * number of days for given month
	 *
	 * @param year
	 * @param month
	 * @return integer containg the number of days in a given month
	 */
	protected int maxDayInMonthFor(int year, int month) {
		//validate days
		if ( month==4 || month==6 || month==9 || month==11 ) {
			return 30;
		}
		else if ( month==2 ) {
			if ( isLeapYear(year) ) {
				return 29;
			}
			else {
				return 28;
			}
		}
		else {
			return 31;
		}
	}
	
	private boolean isLeapYear(int year) {
		
		//REVISIT: should we take care about Julian calendar?
		return((year%4 == 0) && ((year%100 != 0) || (year%400 == 0)));
	}
	
	//
	// help function described in W3C PR Schema [E Adding durations to dateTimes]
	//
	protected int mod (int a, int b, int quotient) {
		//modulo(a, b) = a - fQuotient(a,b)*b
		return (a - quotient*b) ;
	}
	
	//
	// help function described in W3C PR Schema [E Adding durations to dateTimes]
	//
	protected int fQuotient (int a, int b) {
		
		//fQuotient(a, b) = the greatest integer less than or equal to a/b
		return (int)Math.floor((float)a/b);
	}
	
	//
	// help function described in W3C PR Schema [E Adding durations to dateTimes]
	//
	protected int modulo (int temp, int low, int high) {
		//modulo(a - low, high - low) + low
		int a = temp - low;
		int b = high - low;
		return (mod (a, b, fQuotient(a, b)) + low) ;
	}
	
	//
	// help function described in W3C PR Schema [E Adding durations to dateTimes]
	//
	protected int fQuotient (int temp, int low, int high) {
		//fQuotient(a - low, high - low)
		
		return fQuotient(temp - low, high - low);
	}
	
	
	protected String dateToString(DateTimeData date) {
		StringBuffer message = new StringBuffer(25);
		append(message, date.year, 4);
		message.append('-');
		append(message, date.month, 2);
		message.append('-');
		append(message, date.day, 2);
		message.append('T');
		append(message, date.hour, 2);
		message.append(':');
		append(message, date.minute, 2);
		message.append(':');
		append(message, date.second);
		append(message, (char)date.utc, 0);
		return message.toString();
	}
	
	protected final void append(StringBuffer message, int value, int nch) {
        if (value == Integer.MIN_VALUE) {
            message.append(value);
            return;
        }
		if (value < 0) {
			message.append('-');
			value = -value;
		}
		if (nch == 4) {
			if (value < 10)
				message.append("000");
			else if (value < 100)
				message.append("00");
			else if (value < 1000)
				message.append('0');
			message.append(value);
		}
		else if (nch == 2) {
			if (value < 10)
				message.append('0');
			message.append(value);
		}
		else {
			if (value != 0)
				message.append((char)value);
		}
	}
	
	protected final void append(StringBuffer message, double value) {
	    if (value < 0) {
	        message.append('-');
	        value = -value;
	    }
	    if (value < 10) {
	        message.append('0');
	    }
	    append2(message, value);
	}
    
    protected final void append2(StringBuffer message, double value) {
        final int intValue = (int) value;
        if (value == intValue) {
            message.append(intValue);
        }
        else {
            append3(message, value);
        }
    }
    
    private void append3(StringBuffer message, double value) {
        String d = String.valueOf(value);
        int eIndex = d.indexOf('E');
        if (eIndex == -1) {
            message.append(d);
            return;
        }
        int exp;
        if (value < 1) {
            // Need to convert from scientific notation of the form 
            // n.nnn...E-N (N >= 4) to a normal decimal value.
            try {
                exp = parseInt(d, eIndex+2, d.length());
            }
            // This should never happen. 
            // It's only possible if String.valueOf(double) is broken.
            catch (Exception e) {
                message.append(d);
                return;
            }
            message.append("0.");
            for (int i = 1; i < exp; ++i) {
                message.append('0');
            }
            // Remove trailing zeros.
            int end = eIndex - 1;
            while (end > 0) {
                char c = d.charAt(end);
                if (c != '0') {
                    break;
                }
                --end;
            }
            // Now append the digits to the end. Skip over the decimal point.
            for (int i = 0; i <= end; ++i) {
                char c = d.charAt(i);
                if (c != '.') {
                    message.append(c);
                }
            }
        }
        else {
            // Need to convert from scientific notation of the form 
            // n.nnn...EN (N >= 7) to a normal decimal value.
            try {
                exp = parseInt(d, eIndex+1, d.length());
            }
            // This should never happen. 
            // It's only possible if String.valueOf(double) is broken.
            catch (Exception e) {
                message.append(d);
                return;
            }
            final int integerEnd = exp + 2;
            for (int i = 0; i < eIndex; ++i) {
                char c = d.charAt(i);
                if (c != '.') {
                    if (i == integerEnd) {
                        message.append('.');
                    }
                    message.append(c);
                }
            }
            // Append trailing zeroes if necessary.
            for (int i = integerEnd - eIndex; i > 0; --i) {
                message.append('0');
            }
        }
    }
	
	protected double parseSecond(String buffer, int start, int end)
	throws NumberFormatException {
		int dot = -1;
		for (int i = start; i < end; i++) {
			char ch = buffer.charAt(i);
			if (ch == '.')
				dot = i;
			else if (ch > '9' || ch < '0')
				throw new NumberFormatException("'" + buffer + "' has wrong format");
		}
		if (dot == -1) {
			if (start+2 != end)
				throw new NumberFormatException("'" + buffer + "' has wrong format");
		}
		else if (start+2 != dot || dot+1 == end) {
			throw new NumberFormatException("'" + buffer + "' has wrong format");
		}
		return Double.parseDouble(buffer.substring(start, end));
	}
	
	//
	//Private help functions
	//
	
	private void cloneDate (DateTimeData finalValue, DateTimeData tempDate) {
		tempDate.year = finalValue.year;
		tempDate.month = finalValue.month;
		tempDate.day = finalValue.day;
		tempDate.hour = finalValue.hour;
		tempDate.minute = finalValue.minute;
		tempDate.second = finalValue.second;
		tempDate.utc = finalValue.utc;
		tempDate.timezoneHr = finalValue.timezoneHr;
		tempDate.timezoneMin = finalValue.timezoneMin;
	}
	
	/**
	 * Represents date time data
	 */
	static final class DateTimeData implements XSDateTime {
		int year, month, day, hour, minute, utc;
		double second;
		int timezoneHr, timezoneMin;
        private String originalValue;
        boolean normalized = true;
        
        int unNormYear;
        int unNormMonth;
        int unNormDay;
        int unNormHour;
        int unNormMinute;
        double unNormSecond;
		
		// used for comparisons - to decide the 'interesting' portions of
		// a date/time based data type.
		int position;
		// a pointer to the type that was used go generate this data
		// note that this is not the actual simple type, but one of the
		// statically created XXXDV objects, so this won't cause any GC problem.
		final AbstractDateTimeDV type;
		private String canonical;
		public DateTimeData(String originalValue, AbstractDateTimeDV type) {
            this.originalValue = originalValue;
			this.type = type;
		}
		public DateTimeData(int year, int month, int day, int hour, int minute,
				double second, int utc, String originalValue, boolean normalized, AbstractDateTimeDV type) {
			this.year = year;
			this.month = month;
			this.day = day;
			this.hour = hour;
			this.minute = minute;
			this.second = second;
			this.utc = utc;
			this.type = type;
            this.originalValue = originalValue;
		}
        public boolean equals(Object obj) {
			if (!(obj instanceof DateTimeData))
				return false;
			return type.compareDates(this, (DateTimeData)obj, true)==0;
		}
        public synchronized String toString() {
			if (canonical == null) {
				canonical = type.dateToString(this);
			}
			return canonical;
		}
		/* (non-Javadoc)
		 * @see org.apache.xerces.xs.datatypes.XSDateTime#getYear()
		 */
        public int getYears() {
            if(type instanceof DurationDV)
                return 0;
			return normalized?year:unNormYear;
		}
		/* (non-Javadoc)
		 * @see org.apache.xerces.xs.datatypes.XSDateTime#getMonth()
		 */
        public int getMonths() {
            if(type instanceof DurationDV) {
                return year*12 + month;
            }
			return normalized?month:unNormMonth;
		}
		/* (non-Javadoc)
		 * @see org.apache.xerces.xs.datatypes.XSDateTime#getDay()
		 */
        public int getDays() {
            if(type instanceof DurationDV)
                return 0;
			return normalized?day:unNormDay;
		}
		/* (non-Javadoc)
		 * @see org.apache.xerces.xs.datatypes.XSDateTime#getHour()
		 */
        public int getHours() {
            if(type instanceof DurationDV)
                return 0;
			return normalized?hour:unNormHour;
		}
		/* (non-Javadoc)
		 * @see org.apache.xerces.xs.datatypes.XSDateTime#getMinutes()
		 */
        public int getMinutes() {
            if(type instanceof DurationDV)
                return 0;
			return normalized?minute:unNormMinute;
		}
		/* (non-Javadoc)
		 * @see org.apache.xerces.xs.datatypes.XSDateTime#getSeconds()
		 */
        public double getSeconds() {
            if(type instanceof DurationDV) {
                return day*24*60*60 + hour*60*60 + minute*60 + second;
            }
			return normalized?second:unNormSecond;
		}
		/* (non-Javadoc)
		 * @see org.apache.xerces.xs.datatypes.XSDateTime#hasTimeZone()
		 */
        public boolean hasTimeZone() {
			return utc != 0;
		}
		/* (non-Javadoc)
		 * @see org.apache.xerces.xs.datatypes.XSDateTime#getTimeZoneHours()
		 */
        public int getTimeZoneHours() {
			return timezoneHr;
		}
		/* (non-Javadoc)
		 * @see org.apache.xerces.xs.datatypes.XSDateTime#getTimeZoneMinutes()
		 */
        public int getTimeZoneMinutes() {
			return timezoneMin;
		}
        /* (non-Javadoc)
         * @see org.apache.xerces.xs.datatypes.XSDateTime#getLexicalValue()
         */
        public String getLexicalValue() {
            return originalValue;
        }
        /* (non-Javadoc)
         * @see org.apache.xerces.xs.datatypes.XSDateTime#normalize()
         */
        public XSDateTime normalize() {
            if(!normalized) {
                DateTimeData dt = (DateTimeData)this.clone();
                dt.normalized = true;
                return dt;
            }
            return this;
        }
        /* (non-Javadoc)
         * @see org.apache.xerces.xs.datatypes.XSDateTime#isNormalized()
         */
        public boolean isNormalized() {
            return normalized;
        }
        
        public Object clone() {
            DateTimeData dt = new DateTimeData(this.year, this.month, this.day, this.hour, 
                        this.minute, this.second, this.utc, this.originalValue, this.normalized, this.type);
            dt.canonical = this.canonical;
            dt.position = position;
            dt.timezoneHr = this.timezoneHr;
            dt.timezoneMin = this.timezoneMin;
            dt.unNormYear = this.unNormYear;
            dt.unNormMonth = this.unNormMonth;
            dt.unNormDay = this.unNormDay;
            dt.unNormHour = this.unNormHour;
            dt.unNormMinute = this.unNormMinute;
            dt.unNormSecond = this.unNormSecond;
            return dt;
        }
        
        /* (non-Javadoc)
         * @see org.apache.xerces.xs.datatypes.XSDateTime#getXMLGregorianCalendar()
         */
        public XMLGregorianCalendar getXMLGregorianCalendar() {
            return type.getXMLGregorianCalendar(this);
        }
        /* (non-Javadoc)
         * @see org.apache.xerces.xs.datatypes.XSDateTime#getDuration()
         */
        public Duration getDuration() {
            return type.getDuration(this);
        }
	}

    protected XMLGregorianCalendar getXMLGregorianCalendar(DateTimeData data) {
        return null;
    }

    protected Duration getDuration(DateTimeData data) {
        return null;
    }
    
    protected final BigDecimal getFractionalSecondsAsBigDecimal(DateTimeData data) {
        final StringBuffer buf = new StringBuffer();
        append3(buf, data.unNormSecond);
        String value = buf.toString();
        final int index = value.indexOf('.');
        if (index == -1) {
            return null;
        }
        value = value.substring(index);
        final BigDecimal _val = new BigDecimal(value);
        if (_val.compareTo(BigDecimal.valueOf(0)) == 0) {
            return null;
        }
        return _val;
    }
}
